package com.xxx.ratelimiter;

import com.alibaba.druid.util.StringUtils;
import com.xxx.util.JedisUtil;

/**
 * 使用Redis做api限流器
 */
public class RedisLimiter {
	
	/**
	 * 基于redis实现api限流方案一：
	 * 核心思路：用一个list来存放一个列表，key为该api名称，每次请求都把当前时间戳放入list，
	 * 		      如果第1000次调用时的时间戳和第1次时间戳的差小于60s，那么就是60s里调用超1000次；否则清空列表之前的值。
	 * 优点：限流准确，支持分布式环境
	 * 缺点：比较消耗redis内存，并消耗一定的网络交互，强依赖第三方组件redis
	 */
	public static void checkLimit1(String apiKey, int maxTimes, int seconds) {
		 String redisKey = "checkLimit1:"+apiKey;  // redis key为apiKey
		 Long len = JedisUtil.llen(redisKey);
		 // 判断list长度是否达到限流阈值
		 if (len < maxTimes) {  // 未超过限流阀值，将当前时间戳放入list
			 JedisUtil.lpush(redisKey, System.currentTimeMillis()+"");
		 } else { // 超过限流阀值
			 String startTime = JedisUtil.lindex(redisKey, -1); // 获取list的第1个元素的值
			 if ((System.currentTimeMillis() - Long.valueOf(startTime)) < seconds*1000) { // 还在60秒之内
				 throw new RuntimeException(apiKey+"超过限流阈值"); // 触发限流
			 } else {  // 已经超过60秒了，重置限流参数
				 JedisUtil.lpush(redisKey, System.currentTimeMillis()+""); // 将当前时间戳push到list中
				 JedisUtil.ltrim(redisKey, 0, 0); // 清空list
				 JedisUtil.setExpire(redisKey, seconds); // 重新设置过期时间
			 }
		 }
	}
	
	/**
	 * 基于redis实现api限流方案二：
	 * 核心思路：设置一个key，过期时间为1分钟，value是这1分钟内调用api的次数，如果调用次数超过1000，就触发限流开关。
	 * 优点：占用内存较少。
	 * 缺点：只保证在这1分钟之内调用次数低于1000，不保证任意时间段的一分钟之内都低于1000。
	 */
	public static void checkLimit2(String apiKey, int maxTimes, int seconds) {
		 String redisKey = "checkLimit2:"+apiKey;
		 String value = JedisUtil.get(redisKey);
		 Integer currTimes = 0;
		 if(!StringUtils.isEmpty(value)) {
			 currTimes = Integer.parseInt(value);
		 }
		 if (currTimes>=maxTimes) { // 调用次数超过限流阀值，触发限流
		 	throw new RuntimeException(apiKey+"超过限流阈值");
		 }
		 long ttl = JedisUtil.ttl(redisKey);
		 if (ttl>0) { // 未过期
			 JedisUtil.incr(redisKey);
		 } else { // key不存在或已过期，重置限流参数
			 JedisUtil.incr(redisKey);
			 JedisUtil.setExpire(redisKey, seconds);
		 }
	}

}
